﻿using System;
using System.ComponentModel.Composition;
using System.ServiceModel;
using Cubicle.Core.Hosting;
using Cubicle.Core.Interfaces;

namespace Cubicle.Core.Eventing
{
    // Create a service contract and define the service operations.
    // NOTE: The service operations must be declared explicitly.

    // The Service implementation implements your service contract.
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
    [UseNetDataContractSerializer]
    public class SubscribeService : ISubscribeContract
    {
        private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();

        // Repository storing the last data element of each event.
        [Import] private IRepository _repository;

        // Lock object
        private static object _lock = new object();

        public void Subscribe(string handle)
        {
            IRObject wrapper;
            if (!TryGetWrapper(handle, out wrapper)) return;

            if (Subscribe<double>(wrapper)) return;
            if (Subscribe<int>(wrapper)) return;
            if (Subscribe<string>(wrapper)) return;
            if (Subscribe<DateTime>(wrapper)) return;
            if (Subscribe<bool>(wrapper)) return;
        }

        private bool TryGetWrapper(string handle, out IRObject wrapper)
        {
            var target = _repository.Get(handle);
            var targetType = target == null ? null : target.GetType();
            if (target == null || !targetType.IsGenericType ||
                (targetType.GetGenericTypeDefinition() != typeof(IObservable<>)))
            {
                OnError(OperationContext.Current);
                wrapper = null;
                return false;
            }
            wrapper = _repository.GetRObject(handle);
            return true;
        }

        public void Unsubscribe(string handle)
        {
            IRObject wrapper;
            if (!TryGetWrapper(handle, out wrapper)) return;
            var source = OperationContext.Current.Channel.RemoteAddress.Uri.ToString();

            lock (_lock)
            {
                // Check if there are any subscriptions
                if (!wrapper.Metadata.ContainsKey(source))
                {
                    _logger.Error("Could not unsubscribe from handle {0}. Reference not found.", handle);
                    return;
                }

                // Decrement the # subscriptions
                wrapper.Metadata[source] = ((int)wrapper.Metadata[source]) - 1;

                // Remove if zero
                if ((int)wrapper.Metadata[source] == 0)
                    wrapper.Metadata.Remove(source);
            }
        }

        private void OnError(OperationContext context)
        {
            try
            {
                var callback = context.GetCallbackChannel<IClientObserver>();
                callback.OnError("Handle not found or Handle not observable");
            }
            catch (Exception e)
            {
                _logger.ErrorException("Error on subscription", e);
            }
        }

        private bool Subscribe<T>(IRObject wrapper)
        {
            // Do some type checking to confirm the generic type matches the underlying observable type
            var target = wrapper.Object;
            if (!(target is IObservable<T>)) return false;
            var observable = (IObservable<T>) target;

            // Create a new server-side observer wrapper
            var source = OperationContext.Current.Channel.RemoteAddress.Uri.ToString();

            lock (_lock)
            {
                if (wrapper.Metadata.ContainsKey(source))
                {
                    wrapper.Metadata[source] = ((int)wrapper.Metadata[source]) + 1;
                }
                else
                {
                    var observer = new ClientObserver<T>(OperationContext.Current, observable);
                    wrapper.Metadata[source] = 1;
                }
            }

            return true;
        }

    }

    internal class ClientObserver<T> : IObserver<T>
    {
        private OperationContext _context;
        private string _source;
        private IClientObserver _callback;
        private IDisposable _unsubscribe;

        public ClientObserver(OperationContext context, IObservable<T> observable)
        {
            _context = context;
            _source = context.Channel.RemoteAddress.Uri.ToString();
            _callback = context.GetCallbackChannel<IClientObserver>();

            // Keep a reference to the disposable in case the client connection fails
            _unsubscribe = observable.Subscribe(this);
        }

        public void Unsubscribe()
        {
            _unsubscribe.Dispose();
        }

        public void OnNext(T value)
        {
            try
            {
                _callback.OnNext(value);
            }
            catch (Exception e)
            {
                OnError(e);
            }
        }

        public void OnError(Exception error)
        {
            try
            {
                _callback.OnError(error.Message);
            }
            catch
            {
                Unsubscribe();
            }
        }

        public void OnCompleted()
        {
            try
            {
                _callback.OnComplete();
            }
            catch (Exception e)
            {
                OnError(e);
            }
        }
    }

}


